Projet final du Kit Big Data 2022¶

ENDEL BATOKA Herve Donald - Telecom Paris 2022/2023¶

Traitement des données du Vendée Globe 2020-2021¶

Dans cette étude, nous travaillons sur des données issue du Vendée Globe 2020-2021. Les données constituant notre base sont extrait du site web du Vendée Globe. Elles sont ensuite traitées, nettoyées et mises en forme pour de la visualisation et des traitements statistiques dont nous présenterons les conclusions.

Dans la partie analyse et visualisation des données, nous allons entre autre travailler sur des séries temporelles, la gestion des systèmes d'information géographique (pour le monitoring du parcours des voiliers), et d'autres analyses connexes.

Dans un premier temps, les différentes bibliothèques utilisées dans la ce projet, sont importées.

In [1]:
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from datetime import datetime
from bs4 import BeautifulSoup
import requests
import re
import pickle

Acquisition, chargement et préparation des données¶

Nous réalisons une extraction de la liste des fichiers disponible, par scrapping du site www.vendeeglobe.org

In [2]:
html = "https://www.vendeeglobe.org/fr/classement/20210117_110000"
In [40]:
r = requests.get(html)

type(r.content)
content = r.content.decode('utf-8')
soup = BeautifulSoup(content)
In [4]:
import uuid

import xlwings as xw

# lecture/écriture d'un fichier Excel avec xlwings
def save_with_xlwings(file,output_name='{uuid.uuid1()}'):
    tempfile = output_name+'.xlsx'
    excel_app = xw.App(visible=False)
    excel_book = excel_app.books.open(file)
    excel_book.save(tempfile)
    excel_book.close()
    excel_app.quit()
    return tempfile

En récupérant les différentes option de la liste déroulante de la page web, on obtient alors tous les suffixes des liens nous permettant ainsi d'automatiser le téléchargement des fichiers.

In [5]:
li_tag = soup.findAll('option')
link_suffix = []
for tag in li_tag:
    if tag.attrs["value"] != "":
        link_suffix.append(tag.attrs["value"])
print("Il y a au total {} fichiers de classement disponible sur le site vendée globe".format(len(link_suffix)))
Il y a au total 706 fichiers de classement disponible sur le site vendée globe

On se rend compte qu'après l'arrivé de certains voiliers, la structure des fichiers excel change. Alors nous décidons de nous en tenir aux éléments entre le 2021-11-08 et 2021-03-04.

In [6]:
last_date = '20210127_140000'
link_to_use = link_suffix[link_suffix.index(last_date):]
df_temp = pd.DataFrame()
for link in link_to_use:

    url_excel = 'https://www.vendeeglobe.org/download-race-data/vendeeglobe_'+link+'.xlsx'

    from shutil import copyfileobj
    from urllib import request


    filename = link+'.xlsx'

    with request.urlopen(url_excel) as response, open(filename, 'wb') as out_file:
        copyfileobj(response, out_file)

    save_with_xlwings(filename,output_name=link)
    
    df1 = (pd.read_excel(filename,header=3,usecols=[1,2,3,5,6,12,13,19])
       .rename(columns={"Rang\nRank":"Rang","Skipper / Bateau\nSkipper / crew":"Skipper_bateau",
                        "Nat. / Voile\nNat. / Sail":"Voile","Unnamed: 5":"Latitude",
                       "Unnamed: 6":"Longitude", "Unnamed: 12":"Vitesse (kts)",
                        "Unnamed: 13":"VMG (kts)","DTF":"DTF (nm)"})
       .drop(0)       
       .dropna(subset=["Vitesse (kts)"])
       .reset_index(drop=True)
     )
    temp = datetime.strptime(link,"%Y%m%d_%H%M%S")
    df1["date_format"] = temp.strftime("%Y-%m-%d %H:%M:%S")
    
    df_temp = pd.concat([df_temp, df1])
In [7]:
# exampleObj = df_temp

# fileObj = open('df_temp.obj', 'wb')
# pickle.dump(exampleObj,fileObj)
# fileObj.close()
In [8]:
# fileObj = open('df_temp.obj', 'rb')
# df_temp = pickle.load(fileObj)
# fileObj.close()
df_temp
Out[8]:
Rang Voile Skipper_bateau Latitude Longitude Vitesse (kts) VMG (kts) DTF (nm) date_format
0 1 \nFRA 79 Charlie Dalin\nAPIVIA 46°14.07'N 03°41.61'W 19.1 kts 17.4 kts 80.5 nm 2021-01-27 14:00:00
1 2 \nFRA 18 Louis Burton\nBureau Vallée 2 46°24.62'N 05°11.90'W 17.7 kts 16.9 kts 141.5 nm 2021-01-27 14:00:00
2 3 \nMON 10 Boris Herrmann\nSeaexplorer - Yacht Club De Mo... 44°31.46'N 05°20.55'W 16.6 kts 10.3 kts 190.0 nm 2021-01-27 14:00:00
3 4 \nFRA 59 Thomas Ruyant\nLinkedOut 47°24.42'N 07°22.08'W 17.6 kts 17.6 kts 235.8 nm 2021-01-27 14:00:00
4 5 \nFRA 17 Yannick Bestaven\nMaître Coq IV 47°43.38'N 07°58.81'W 18.1 kts 18.1 kts 264.3 nm 2021-01-27 14:00:00
... ... ... ... ... ... ... ... ... ...
26 27 \nFRA 72 Alexia Barrier\nTSE - 4myplanet 46°25.83'N 01°48.92'W 0.0 kts 0.0 kts 24295.4 nm 2020-11-08 14:00:00
27 28 \nFRA 27 Isabelle Joschke\nMACSF 46°24.98'N 01°48.22'W 0.0 kts 0.0 kts 24295.5 nm 2020-11-08 14:00:00
28 29 \nFRA 4 Sébastien Simon\nARKEA PAPREC 46°25.75'N 01°48.73'W 0.0 kts 0.0 kts 24295.5 nm 2020-11-08 14:00:00
29 30 \nFRA 50 Miranda Merron\nCampagne de France 46°25.39'N 01°48.34'W 0.0 kts 0.0 kts 24295.6 nm 2020-11-08 14:00:00
30 31 \nFIN 222 Ari Huusela\nStark 46°25.65'N 01°48.21'W 0.0 kts 0.0 kts 24295.8 nm 2020-11-08 14:00:00

13784 rows × 9 columns

In [9]:
df = df_temp.copy()
df.head(5)
Out[9]:
Rang Voile Skipper_bateau Latitude Longitude Vitesse (kts) VMG (kts) DTF (nm) date_format
0 1 \nFRA 79 Charlie Dalin\nAPIVIA 46°14.07'N 03°41.61'W 19.1 kts 17.4 kts 80.5 nm 2021-01-27 14:00:00
1 2 \nFRA 18 Louis Burton\nBureau Vallée 2 46°24.62'N 05°11.90'W 17.7 kts 16.9 kts 141.5 nm 2021-01-27 14:00:00
2 3 \nMON 10 Boris Herrmann\nSeaexplorer - Yacht Club De Mo... 44°31.46'N 05°20.55'W 16.6 kts 10.3 kts 190.0 nm 2021-01-27 14:00:00
3 4 \nFRA 59 Thomas Ruyant\nLinkedOut 47°24.42'N 07°22.08'W 17.6 kts 17.6 kts 235.8 nm 2021-01-27 14:00:00
4 5 \nFRA 17 Yannick Bestaven\nMaître Coq IV 47°43.38'N 07°58.81'W 18.1 kts 18.1 kts 264.3 nm 2021-01-27 14:00:00
In [10]:
df["Voile"] = df.Voile.apply(lambda x: re.compile(r'\n(.*)').search(x).group(1))
df["Skipper"] = df.Skipper_bateau.apply(lambda x: re.compile(r'(.*)\n(.*)').search(x).group(1))
df["Bateau"] = df.Skipper_bateau.apply(lambda x: re.compile(r'(.*)\n(.*)').search(x).group(2))
df["Vitesse (kts)"] = df["Vitesse (kts)"].apply(lambda x: re.compile(r'(.*) (.*)').search(x).group(1)).astype(float)
df["VMG (kts)"] = df["VMG (kts)"].apply(lambda x: re.compile(r'(.*) (.*)').search(x).group(1)).astype(float)
df["DTF (nm)"] = df["DTF (nm)"].apply(lambda x: re.compile(r'(.*) (.*)').search(x).group(1)).astype(float)
df["Rang"] = df["Rang"].astype(int)
df = df.drop(columns="Skipper_bateau")
df = df[["date_format","Rang","Voile","Skipper","Bateau","Vitesse (kts)","VMG (kts)","DTF (nm)","Latitude","Longitude",]]

On réalise plusieurs opération de traitement de nos fichiers excels individuellement, notamment :

  • On renomme les colonnes
  • On supprime les lignes pour lesquelles on n'a pas d'information comme la vitesse

Puis on fait un traitement finale qui consiste à :

  • Concatenener les fichiers
  • mettre en forme les dates selon un format conventionnel, et de la même manière on anticipe sur le traitement des séries temporelles
  • Changer les types des colonnes numériques
  • Créer une colonne séparer pour les noms des bateaux

Le dataframe finale est présenté ci-dessous.

In [11]:
df
Out[11]:
date_format Rang Voile Skipper Bateau Vitesse (kts) VMG (kts) DTF (nm) Latitude Longitude
0 2021-01-27 14:00:00 1 FRA 79 Charlie Dalin APIVIA 19.1 17.4 80.5 46°14.07'N 03°41.61'W
1 2021-01-27 14:00:00 2 FRA 18 Louis Burton Bureau Vallée 2 17.7 16.9 141.5 46°24.62'N 05°11.90'W
2 2021-01-27 14:00:00 3 MON 10 Boris Herrmann Seaexplorer - Yacht Club De Monaco 16.6 10.3 190.0 44°31.46'N 05°20.55'W
3 2021-01-27 14:00:00 4 FRA 59 Thomas Ruyant LinkedOut 17.6 17.6 235.8 47°24.42'N 07°22.08'W
4 2021-01-27 14:00:00 5 FRA 17 Yannick Bestaven Maître Coq IV 18.1 18.1 264.3 47°43.38'N 07°58.81'W
... ... ... ... ... ... ... ... ... ... ...
26 2020-11-08 14:00:00 27 FRA 72 Alexia Barrier TSE - 4myplanet 0.0 0.0 24295.4 46°25.83'N 01°48.92'W
27 2020-11-08 14:00:00 28 FRA 27 Isabelle Joschke MACSF 0.0 0.0 24295.5 46°24.98'N 01°48.22'W
28 2020-11-08 14:00:00 29 FRA 4 Sébastien Simon ARKEA PAPREC 0.0 0.0 24295.5 46°25.75'N 01°48.73'W
29 2020-11-08 14:00:00 30 FRA 50 Miranda Merron Campagne de France 0.0 0.0 24295.6 46°25.39'N 01°48.34'W
30 2020-11-08 14:00:00 31 FIN 222 Ari Huusela Stark 0.0 0.0 24295.8 46°25.65'N 01°48.21'W

13784 rows × 10 columns

Récupération des informations des bateaux¶

Egalement par scrapping, on récupère les fiches techniques de tous les bateaux

In [12]:
html_b = 'https://www.vendeeglobe.org/fr/glossaire'
r_b = requests.get(html_b)
content_b = r_b.content.decode('utf-8')
soup_b = BeautifulSoup(content_b)

On vérifie bien que la class "boats-list__popup-title" nous permet bien d'obtenir les noms des bateaux, en vérifiant sur un cas.

In [13]:
soup_b.find('h3',{'class': "boats-list__popup-title"}).text
Out[13]:
'NEWREST - ART & FENÊTRES'

On peut à présent généraliser la récupération des informations du bateau. Ces informations sont d'abord stockées dans un dictionnaire dont les clés et les valeurs associées sont présentés ci-après.

In [14]:
boats_list = soup_b.find_all('h3',{'class': "boats-list__popup-title"})
In [41]:
dico_bateau = {}
for item in boats_list:
    #print("Clé : {}".format(item.text))
    dico_bateau_key = item.text
    dico_temp = {}
    for item_li in item.find_next().find_all('li'):
        #print("\tValeur : ({})".format(item_li.text))
        dico_key = re.compile(r'(.*) : (.*)').search(item_li.text).group(1)
        dico_val = re.compile(r'(.*) : (.*)').search(item_li.text).group(2)
        dico_temp.update({dico_key:dico_val})
    dico_bateau_value = dico_temp
    dico_bateau.update({dico_bateau_key:dico_bateau_value})

On crée un dictionnaire composé, dont les clés sont les noms des bateaux et les valeurs sont des dictionnaires contenant les caratéristiques des bateaux

In [16]:
print("Il y a au total {} bateau inscrit à la comptétion".format(len(dico_bateau)))
Il y a au total 34 bateau inscrit à la comptétion

On remarquera plus aisément dans la suite qu'il y a un bateau dont la seule information est la date de lancement. Cela nous pousse à croire en une erreur dans la base. Néanmoins, cela ne nous empêche pas de travailler.

In [42]:
# dico_bateau

On peut aussi constater que les noms des bateaux dans la fiches technique et dans les fichiers excels du classement ont quelques différences, que nous nous donnons pour objectifs de corriger.

Pour cela, on va construire un dicotionnaire que correspondance que nous allons par la suite mapper sur l'un des dataframes.

In [18]:
df_bateau = pd.DataFrame.from_dict(dico_bateau).T
df_bateau = df_bateau.reset_index()
df_bateau["Boat"] = df_bateau["index"].str.capitalize()

df["boat"] = df.Bateau.str.capitalize()
In [19]:
dico_map = {np.sort(df_bateau["Boat"].unique())[1:][i]:np.sort(df["boat"].unique())[i] for i in range(len(df_bateau)-1)}
dico_map
Out[19]:
{'Apivia': 'Apivia',
 'Arkea paprec': 'Arkea paprec',
 'Banque populaire x': 'Banque populaire x',
 'Bureau vallee 2': 'Bureau vallée 2',
 'Campagne de france': 'Campagne de france',
 'Charal': 'Charal',
 'Compagnie du lit / jiliti': 'Compagnie du lit - jiliti',
 "Corum l'epargne": "Corum l'épargne",
 'Dmg mori global one': 'Dmg mori global one',
 'Groupe apicil': 'Groupe apicil',
 'Groupe sétin': 'Groupe sétin',
 'Hugo boss': 'Hugo boss',
 'Initiatives-coeur': 'Initiatives - coeur',
 "L'occitane en provence": "L'occitane en provence",
 'La fabrique': 'La fabrique',
 'La mie câline - artisans artipôle': 'La mie câline - artisans artipôle',
 'Linkedout': 'Linkedout',
 'Macsf': 'Macsf',
 'Maître coq iv': 'Maître coq iv',
 'Medallia': 'Medallia',
 'Merci': 'Merci',
 'Newrest - art & fenêtres': 'Newrest - art et fenetres',
 'Omia - water family ': 'Omia - water family',
 'One planet one ocean': 'One planet one ocean',
 'Prb': 'Prb',
 'Prysmian group': 'Prysmian group',
 'Pure - best western®': 'Pure - best western hotels and resorts',
 'Seaexplorer - yacht club de monaco': 'Seaexplorer - yacht club de monaco',
 'Stark': 'Stark',
 'Time for oceans': 'Time for oceans',
 'Tse -  4myplanet': 'Tse - 4myplanet',
 'V and b-mayenne': 'V and b mayenne',
 'Yes we cam!': 'Yes we cam !'}

La correspondance étant vérifiées, on réalise la mapping des noms sur le dataframe des caractéristique bateau. Ce choix est stratégique car c'est le dataframe le plus petit. Le temps d'exécution est donc meilleurs.

In [43]:
df_bateau.Boat = df_bateau.Boat.map(dico_map)
df_bateau.head(10)
Out[43]:
index Numéro de voile Anciens noms du bateau Architecte Chantier Date de lancement Longueur Largeur Tirant d'eau Déplacement (poids) Nombre de dérives Hauteur mât Voile quille Surface de voiles au près Surface de voiles au portant Chantier : CDK Technologies / Assemblage Boat
0 NEWREST - ART & FENÊTRES FRA 56 No Way Back, Vento di Sardegna VPLP/Verdier Persico Marine 01 Août 2015 18,28 m 5,85 m 4,50 m 7 t foils 29 m monotype 320 m2 570 m2 NaN NaN
1 PURE - Best Western® FRA 49 Gitana Eighty, Synerciel, Newrest-Matmut Bruce Farr Design Southern Ocean Marine (Nouvelle Zélande) 08 Mars 2007 18,28m 5,80m 4,50m 9t 2 28m acier forgé 280 m2 560 m2 NaN NaN
2 TSE - 4MYPLANET FRA72 Famille Mary-Etamine du Lys, Initiatives Coeur... Marc Lombard MAG France 01 Mars 1998 18,28m 5,54m 4,50m 9t 2 29 m acier 260 m2 580 m2 NaN NaN
3 Maître CoQ IV 17 Safran 2 - Des Voiles et Vous Verdier - VPLP CDK Technologies 12 Mars 2015 18,28 m 5,80 m 4,50 m 8 t foils 29 m acier mécano soudé 310 m2 550 m2 NaN Maître coq iv
4 CHARAL 08 NaN VPLP CDK Technologies 18 Août 2018 18,28 m 5,85 m 4,50 m 8t foils 29 m acier 320 m2 600 m2 NaN Charal
5 LA MIE CÂLINE - ARTISANS ARTIPÔLE FRA 14 Ecover3, Président, Gamesa, Kilcullen Voyager-... Owen Clarke Design LLP - Clay Oliver Hakes Marine - Mer Agitée 03 Août 2007 18,28 m 5,65 m 4,50 m 7,9 tonnes foils 29 m basculante avec vérin 300 m² 610 m² NaN La mie câline - artisans artipôle
6 BUREAU VALLEE 2 18 Banque Populaire VIII Verdier - VPLP CDK Technologies 09 Juin 2015 18,28 m 5,80 m 4,50 m 7,6 t foils 28 m acier 300 m2 600 m2 NaN NaN
7 ONE PLANET ONE OCEAN ESP 33 Kingfisher - Educacion sin Fronteras - Forum M... Owen Clarke Design Martens Yachts 02 Février 2000 18,28 m 5,30 m 4,50 m 8,9 t 2 26 m acier 240 m2 470 m2 NaN One planet one ocean
8 GROUPE SÉTIN FRA 71 Paprec-Virbac2, Estrella Damm, We are Water, L... Bruce Farr Yacht Design Southern Ocean Marine (Nouvelle-Zélande) 02 Février 2007 18,28 m 5,80 m 4,50 m 9 t 2 asymétriques 28,50 basculante sur vérin hydraulique 270 m2 560 m2 NaN Groupe sétin
9 BANQUE POPULAIRE X FRA30 Macif - SMA Verdier - VPLP CDK - Mer Agitée 01 Mars 2011 18,28 m 5,70 m 4,5 m 7,7 t 2 29 m acier forgé 340 m2 570 m2 NaN Banque populaire x
In [21]:
df_bateau["Boat"].unique()
Out[21]:
array(['Newrest - art et fenetres',
       'Pure - best western hotels and resorts', 'Tse - 4myplanet',
       'Maître coq iv', 'Charal', 'La mie câline - artisans artipôle',
       'Bureau vallée 2', 'One planet one ocean', 'Groupe sétin',
       'Banque populaire x', 'Apivia', 'Initiatives - coeur', 'Merci',
       'Omia - water family', 'Prb', 'Compagnie du lit - jiliti', nan,
       'Medallia', 'Seaexplorer - yacht club de monaco', 'Stark', 'Macsf',
       'Yes we cam !', 'Time for oceans', 'Campagne de france',
       'Prysmian group', 'La fabrique', 'Linkedout', 'Groupe apicil',
       'Dmg mori global one', 'Arkea paprec', 'V and b mayenne',
       'Hugo boss', "L'occitane en provence", "Corum l'épargne"],
      dtype=object)

On vérifie bien que le mapping a été réalisé avec succès sur le dataframe.

A présent, on fait un rapprochement entre les 2 dataframes pour obtenir un unique comportant les classements et les caractéristique des bateaux associés.

In [22]:
df_final = (pd.merge(df,df_bateau,left_on="boat",right_on="Boat")
           .reset_index(drop=True))
In [23]:
df_final
Out[23]:
date_format Rang Voile Skipper Bateau Vitesse (kts) VMG (kts) DTF (nm) Latitude Longitude ... Largeur Tirant d'eau Déplacement (poids) Nombre de dérives Hauteur mât Voile quille Surface de voiles au près Surface de voiles au portant Chantier : CDK Technologies / Assemblage Boat
0 2021-01-27 14:00:00 1 FRA 79 Charlie Dalin APIVIA 19.1 17.4 80.5 46°14.07'N 03°41.61'W ... 5,85 m 4,50 m 8 t foils 29 m acier 350 m2 560 m2 NaN Apivia
1 2021-01-27 11:00:00 1 FRA 79 Charlie Dalin APIVIA 19.4 19.0 128.9 45°28.35'N 04°30.86'W ... 5,85 m 4,50 m 8 t foils 29 m acier 350 m2 560 m2 NaN Apivia
2 2021-01-27 08:00:00 1 FRA 79 Charlie Dalin APIVIA 19.1 19.1 184.9 44°44.72'N 05°25.61'W ... 5,85 m 4,50 m 8 t foils 29 m acier 350 m2 560 m2 NaN Apivia
3 2021-01-27 04:00:00 1 FRA 79 Charlie Dalin APIVIA 13.4 11.4 261.0 43°53.46'N 06°44.90'W ... 5,85 m 4,50 m 8 t foils 29 m acier 350 m2 560 m2 NaN Apivia
4 2021-01-26 21:00:00 1 FRA 79 Charlie Dalin APIVIA 11.2 11.2 337.8 43°56.62'N 08°55.23'W ... 5,85 m 4,50 m 8 t foils 29 m acier 350 m2 560 m2 NaN Apivia
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
13779 2020-11-09 04:00:00 5 FRA 6 Nicolas Troussel CORUM L'Épargne 14.7 4.7 24137.1 46°30.70'N 07°33.22'W ... 5,70 m 4,50 m 7,9 t foils 27,30 m NaN 270 m2 535 m2 NaN Corum l'épargne
13780 2020-11-08 21:00:00 2 FRA 6 Nicolas Troussel CORUM L'Épargne 17.9 15.7 24179.5 46°14.80'N 05°05.76'W ... 5,70 m 4,50 m 7,9 t foils 27,30 m NaN 270 m2 535 m2 NaN Corum l'épargne
13781 2020-11-08 17:00:00 6 FRA 6 Nicolas Troussel CORUM L'Épargne 22.9 19.4 24239.0 46°22.46'N 03°22.50'W ... 5,70 m 4,50 m 7,9 t foils 27,30 m NaN 270 m2 535 m2 NaN Corum l'épargne
13782 2020-11-08 15:00:00 6 FRA 6 Nicolas Troussel CORUM L'Épargne 19.6 18.2 24267.4 46°21.97'N 02°32.77'W ... 5,70 m 4,50 m 7,9 t foils 27,30 m NaN 270 m2 535 m2 NaN Corum l'épargne
13783 2020-11-08 14:00:00 20 FRA 6 Nicolas Troussel CORUM L'Épargne 0.0 0.0 24295.2 46°25.80'N 01°49.19'W ... 5,70 m 4,50 m 7,9 t foils 27,30 m NaN 270 m2 535 m2 NaN Corum l'épargne

13784 rows × 28 columns

In [24]:
print(len(df_final)-len(df)) 
0

On vérifie bien qu'entre le dataframe original (df) et le dataframe finale comportant les caractéristique du bateau (df_final) on a le même nombre de ligne.

Il n'y a donc pas de perte d'information.

Analyse et Story telling¶

C'est dans cette partie que nous nous focalisons sur les informations que nous pouvons tirer de notre jeu de données.

Dans un premier temps, nous réalisons l'analyse de la présence du Foil sur le classement et la vitesse des voiliers.

Impact du foil¶

Pour ce faire, nous faisons quelques traitement supplémentaire pour se construire spécifique. Nous partons du dataframe final, supprimons des colonnes non essentiel et en rajoutons certaines, comme celle de la présence du foils.

Nous pouvons remarqué, comme présenté ci-dessous, que le foil est décrit par la caractéristique du nombre de dérives. En observant les l'ensemble des éléments on peut donc créer par mapping une nouvelle colonne ayant "Oui" si le voilier est équipé d'un foil ou "Non" si ce n'est pas le cas.

In [25]:
df_last = df_final.copy()
df_last["Nombre de dérives"].unique()
Out[25]:
array(['foils', '2', '2 asymétriques', 'foiler'], dtype=object)
In [26]:
df_last = df_final.copy()
df_last["foils"] = df_last["Nombre de dérives"].map({"foils":"Oui",'2':"Non",'2 asymétriques':"Non",'foiler':"Non"})
df_last["foils_colors"] = df_last["Nombre de dérives"].map({"foils":"Blue",'2':"Red",'2 asymétriques':"Red",'foiler':"Red"})
df_last = df_last.loc[df_final.date_format == '2021-01-27 11:00:00']
df_last = df_last.reset_index(drop=True)
df_last_filter = df_last[["Rang","Skipper","Bateau","VMG (kts)","foils","foils_colors"]]

Pour la forme on rajoute également une colonne couleur qui dans la visualisation nous permettra de différencier les voiliers ayant ou pas le foil. Le résultat est présencté dans la figure ci-dessous. On restreint l'analyse au 27 janvier 2021 à 11h.

In [27]:
fig,ax =  plt.subplots(figsize=(8,6))
x1 = df_last_filter.loc[df_last_filter["foils_colors"]=="Blue"]
x2 = df_last_filter.loc[df_last_filter["foils_colors"]=="Red"]
ax.scatter(x1["Rang"],x1["VMG (kts)"],c=x1["foils_colors"])
ax.scatter(x2["Rang"],x2["VMG (kts)"],c=x2["foils_colors"])
plt.xlabel("Rang")
plt.ylabel("Vitesse Utile (kts)")
plt.legend(["Avec Foil","Sans Foil"])
ax.axvline(x=5.5, ymin=0, linewidth=1, color="k", linestyle = '--')
ax.axvline(x=18.5, ymin=0, linewidth=1, color="k", linestyle = '--')
plt.show;

Il se forme une sorte de cluster. On remarque que les 5 premiers possède un foil, les 6 derniers n'en possède pas, et au milieu de ceux la, on a une zone d'incertitude.

Le foil serait donc une caractéristique importante qui donnerait plus de chance aux voiliers de remporter la course.

Séries temporelles¶

Nous poursuivons par une analyse de séries temporelles.

Dans le premier exemple, nous suivons l'évolution dans le temps de rang des 2 premiers skippers.

On extrait donc de notre dataframe final, les informations relatives aux skippers ciblés.

In [28]:
df0_temp = df_final.loc[df_final["Skipper"] == df["Skipper"].unique()[0]]
df1_temp = df_final.loc[df_final["Skipper"] == df["Skipper"].unique()[1]]
In [29]:
df0_temp = df0_temp.sort_values(by=["date_format"], ascending=True)
temp = df0_temp.date_format.apply(lambda x: datetime.strptime(x,"%Y-%m-%d %H:%M:%S"))

df1_temp = df1_temp.sort_values(by=["date_format"], ascending=True)
temp = df1_temp.date_format.apply(lambda x: datetime.strptime(x,"%Y-%m-%d %H:%M:%S"))
In [30]:
fig,ax = plt.subplots(figsize=(8,6))
ax.scatter(df0_temp.date_format,df0_temp["Rang"])
ax.scatter(df1_temp.date_format[1:],df1_temp["Rang"][1:])
plt.xticks(df0_temp.date_format[::25])
plt.xticks(rotation = 45, ha="right")
plt.ylabel('Rang')
plt.legend([df0_temp.Skipper.iloc[0],df1_temp.Skipper.iloc[0]])
ax.axhline(y = 1, linestyle = '--',color="k")
plt.show;

Dans ce graphique, la première place est mise en évidence par le trait horizontal noir.

Remarque : La fin de la course est vers la gauche du graphique et le début vers la droite

On constate au départ qu'il y a une grosse incertitude sur le positionnement; et à mesure que le temps passe, on voit bien que le skipper Chalie Dalin conserve son avantage, une majeur partie du temps.

Dans le deuxième exemple, on observe l'évolution de la vitesse de Charlie Dalin au cours du temps. Comme le montre le graphique ci-dessous, on ne peut pas tirer de conclusion, ci ce n'est que la vitesse varie en moyenne autre de 14.71 kts

In [31]:
print("la vitesse moyenne de Charlie Dalin est de : {:.2f}".format(df0_temp["Vitesse (kts)"].mean()))
la vitesse moyenne de Charlie Dalin est de : 14.71
In [32]:
fig,ax = plt.subplots(figsize=(8,6))
temp = df0_temp.date_format.apply(lambda x: datetime.strptime(x,"%Y-%m-%d %H:%M:%S"))
plt.plot(temp.apply(lambda x: x.strftime("%d-%m-%Y")),df0_temp["Vitesse (kts)"])
plt.xticks(temp.apply(lambda x: x.strftime("%d-%m-%Y"))[::25]);
plt.yticks(np.arange(min(df0_temp["Vitesse (kts)"]), max(df0_temp["Vitesse (kts)"])+1, 2.0))
plt.ylabel('Vitesse (kts)')
plt.legend({df0_temp.Skipper.iloc[0]})
plt.xticks(rotation = 45, ha="right");

Système d'information géographique¶

A présent, nous nous intéressons au parcours géographique des voiliers. Nous allons donc présenté dans le parcours d'un skipper

Nous disposons dans notre dataframe des coordonnées GPS (latitude, longitude) en format degré minute seconde. Nous les convertissons en nombre décimaux pour pouvoir les afficher dans la carte.

On crée donc de nouvelles colonnes comportant les coordonnées décimales des positions du voilier au file du temps.

In [33]:
E = "E"
W = "W"
N = "N"
S = "S"
def dms2dec(deg, mn, sec,sens):
    if sens == "W" or sens == "S":
        poid = -1
    else:
        poid = 1
    nbre = (deg + mn/60 + sec/3600)*poid
    return nbre
In [34]:
df_final["lon_split"] = df_final.Longitude.apply(lambda x: tuple(re.split("°|\.|'",x)))
df_final["coor_y"] = df_final.lon_split.apply(lambda x: dms2dec(deg=int(str(x[0])),mn=int(str(x[1])),sec=int(str(x[2])),sens=x[3]))
df_final["lat_split"] = df_final.Latitude.apply(lambda x: tuple(re.split("°|\.|'",x)))
df_final["coor_x"] = df_final.lat_split.apply(lambda x: dms2dec(deg=int(str(x[0])),mn=int(str(x[1])),sec=int(str(x[2])),sens=x[3]))
In [37]:
print("On retrace le parcours du skipper : {}".format(df["Skipper"].unique()[0]))
print("Ces information sont présentés ci-bas")
On retrace le parcours du skipper : Charlie Dalin
Ces information sont présentés ci-bas

Une fois de dataframe traiter, nous pouvons réaliser la visualisation grâce à l'API Pyplot. Le résultat du parcours du skipper peut être observé dans l'image qui suit.

In [38]:
df_final.loc[(df_final["Skipper"] == df["Skipper"].unique()[0]) & (df_final["Skipper"] == df["Skipper"].unique()[0])]
Out[38]:
date_format Rang Voile Skipper Bateau Vitesse (kts) VMG (kts) DTF (nm) Latitude Longitude ... Hauteur mât Voile quille Surface de voiles au près Surface de voiles au portant Chantier : CDK Technologies / Assemblage Boat lon_split coor_y lat_split coor_x
0 2021-01-27 14:00:00 1 FRA 79 Charlie Dalin APIVIA 19.1 17.4 80.5 46°14.07'N 03°41.61'W ... 29 m acier 350 m2 560 m2 NaN Apivia (03, 41, 61, W) -3.700278 (46, 14, 07, N) 46.235278
1 2021-01-27 11:00:00 1 FRA 79 Charlie Dalin APIVIA 19.4 19.0 128.9 45°28.35'N 04°30.86'W ... 29 m acier 350 m2 560 m2 NaN Apivia (04, 30, 86, W) -4.523889 (45, 28, 35, N) 45.476389
2 2021-01-27 08:00:00 1 FRA 79 Charlie Dalin APIVIA 19.1 19.1 184.9 44°44.72'N 05°25.61'W ... 29 m acier 350 m2 560 m2 NaN Apivia (05, 25, 61, W) -5.433611 (44, 44, 72, N) 44.753333
3 2021-01-27 04:00:00 1 FRA 79 Charlie Dalin APIVIA 13.4 11.4 261.0 43°53.46'N 06°44.90'W ... 29 m acier 350 m2 560 m2 NaN Apivia (06, 44, 90, W) -6.758333 (43, 53, 46, N) 43.896111
4 2021-01-26 21:00:00 1 FRA 79 Charlie Dalin APIVIA 11.2 11.2 337.8 43°56.62'N 08°55.23'W ... 29 m acier 350 m2 560 m2 NaN Apivia (08, 55, 23, W) -8.923056 (43, 56, 62, N) 43.950556
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
479 2020-11-09 08:00:00 25 FRA 79 Charlie Dalin APIVIA 13.3 0.0 24147.6 46°54.58'N 08°16.41'W ... 29 m acier 350 m2 560 m2 NaN Apivia (08, 16, 41, W) -8.278056 (46, 54, 58, N) 46.916111
480 2020-11-09 04:00:00 17 FRA 79 Charlie Dalin APIVIA 14.9 3.6 24146.9 46°35.71'N 07°14.42'W ... 29 m acier 350 m2 560 m2 NaN Apivia (07, 14, 42, W) -7.245000 (46, 35, 71, N) 46.603056
481 2020-11-08 21:00:00 5 FRA 79 Charlie Dalin APIVIA 14.9 13.0 24185.7 46°10.33'N 04°47.42'W ... 29 m acier 350 m2 560 m2 NaN Apivia (04, 47, 42, W) -4.795000 (46, 10, 33, N) 46.175833
482 2020-11-08 17:00:00 2 FRA 79 Charlie Dalin APIVIA 23.3 20.6 24235.6 46°15.92'N 03°21.99'W ... 29 m acier 350 m2 560 m2 NaN Apivia (03, 21, 99, W) -3.377500 (46, 15, 92, N) 46.275556
483 2020-11-08 15:00:00 2 FRA 79 Charlie Dalin APIVIA 0.0 0.0 24265.8 46°17.46'N 02°31.45'W ... 29 m acier 350 m2 560 m2 NaN Apivia (02, 31, 45, W) -2.529167 (46, 17, 46, N) 46.296111

484 rows × 32 columns

In [39]:
import plotly.graph_objects as go
df0 = df_final.loc[(df_final["Skipper"] == df["Skipper"].unique()[0])]
fig = go.Figure(data=go.Scattergeo(
        lat = df0['coor_x'],
        lon = df0['coor_y'],
        mode = 'markers',    
        marker_color = df0['Skipper'].apply(lambda x: hash(x)),
        text = "Rang" + " : " + str(df0['Rang'][0]) + " " + df0['Skipper'][0] + ", " + df0['Bateau'][0] + ", " + str(df0['VMG (kts)'][0]) + " kts"
        ))

fig.update_layout(
        geo = dict(
            projection_type="natural earth",#"natural earth","orthographic"
            showland = True, landcolor = "rgb(245, 250, 220)",
            showocean=True, oceancolor="rgb(210, 255, 255)",#"rgb(210, 255, 255)"
            showlakes = True, lakecolor = "rgb(240, 255, 255)",
            showcountries=True, countrycolor="Black",
        
        lonaxis = dict(
            showgrid = True,
            dtick = 5
        ),
        lataxis = dict (
            showgrid = True,
            dtick = 5
        )
        ),
    
    )
fig.update_layout(height=500, margin={"r":0,"t":0,"l":0,"b":0})
fig.show()
In [46]:
import plotly.graph_objects as go
df0 = df_final
fig = go.Figure(data=go.Scattergeo(
        lat = df0['coor_x'],
        lon = df0['coor_y'],
        mode = 'markers',    
        marker_color = df0['Skipper'].apply(lambda x: hash(x)),
        ))

fig.update_layout(
        geo = dict(
            projection_type="natural earth",#"natural earth","orthographic"
            showland = True, landcolor = "rgb(245, 250, 220)",
            showocean=True, oceancolor="rgb(210, 255, 255)",#"rgb(210, 255, 255)"
            showlakes = True, lakecolor = "rgb(240, 255, 255)",
            showcountries=True, countrycolor="Black",
        
        lonaxis = dict(
            showgrid = True,
            dtick = 5
        ),
        lataxis = dict (
            showgrid = True,
            dtick = 5
        )
        ),
    
    )
fig.update_layout(height=500, margin={"r":0,"t":0,"l":0,"b":0})
fig.show()

Cette visualisation vient donc terminer le notre étude sur le jeu de données du Vendée Globe 2020-2021